/**
 *  Copyright (c) Filipe Manana <fdmanana@gmail.com>
 *  All rights reserved.
 * 
 *  This file is part of jsdet.
 *
 *  jsdet is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  jsdet is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with jsdet.  If not, see <http://www.gnu.org/licenses/>.
 *
 */


package jsdet.parsing.reporting;

import java.util.*;
import java.text.*;

import jsdet.parsing.GlobalParsingStats;
import jsdet.parsing.enums.ClassMemberType;


/**
 * 
 * @author Filipe Manana
 *
 */
public class HtmlGUIReporter extends AbstractReporter {

    private static final String NEWLINE = "\r\n";
    private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("E dd MMM yyyy HH:mm:ss");


    @Override
    public void reportGlobalParsingStats(GlobalParsingStats stats) {
        classesAlreadyReported.clear();

        basicInfo(stats);

        if ( areFunctionsReported() ) {
            System.out.print(NL(1));
            reportFunctions(stats);
        }

        if ( areClosureObjectsReported() ) {
            System.out.print(NL(1));
            reportClosureObjects(stats);
        }

        System.out.print(NL(1));
        reportClasses(stats);
    }


    private void basicInfo(GlobalParsingStats stats) {
        printVariable(0, "runon", dateFormatter.format(new Date()));
        printVariable(0, "args", originalArgs);

        if ( areFunctionsReported() ) {
            printVariable(0, "totalFunctions", stats.getFunctions().size());
        }

        if ( areClosureObjectsReported() ) {
            printVariable(0, "totalClosureObjects", stats.getClosureObjects().size());
        }

        printVariable(0, "totalClasses", stats.getClasses().size());
    }


    @SuppressWarnings("unchecked")
    private void reportClasses(GlobalParsingStats stats) {
        Map classesMap = new LinkedHashMap();
        Set<String> classes = null;

        switch ( classesSortOrder ) {
            case ASC:
                classes = stats.getClassesOrderedByHierarchyLevelAsc();
                break;

            case DESC:
                classes = stats.getClassesOrderedByHierarchyLevelDesc();
                break;

            case DEFAULT:
            default:
                classes = stats.getClasses();
        }

        for ( String className : classes ) {

            if ( classesAlreadyReported.contains(className) ) {
                continue;
            }

            if ( !isClassInFilter(className) ) {
                continue;
            }

            Map classMap = getClassMap(className, stats);

            if ( classesSortOrder == ClassesSortOrder.DEFAULT ) {
                List<String> upperClasses = stats.getClassBaseHierarchy(className);

                for ( int i = upperClasses.size() - 1; i >= 0; i-- ) {
                    String upperClass = upperClasses.get(i);

                    if ( !stats.hasClass(upperClass) ) {
                        continue;
                    }

                    if ( classesAlreadyReported.contains(upperClass) ) {
                        continue;
                    }

                    if ( !isClassInFilter(upperClass) ) {
                        continue;
                    }

                    classesMap.put(upperClass, getClassMap(upperClass, stats));
                    classesAlreadyReported.add(upperClass);
                }
            }

            classesMap.put(className, classMap);
            classesAlreadyReported.add(className);
        }

        printVariable(0, "classes", classesMap);
    }


    @SuppressWarnings("unchecked")
    private Map getClassMap(String className, GlobalParsingStats stats) {
        Map classMap = new LinkedHashMap();
        Map membersMap = new LinkedHashMap();
        int nonfunctionMembersCount = stats.getClassMembers(className, ClassMemberType.NON_FUNCTION).size();
        int functionMembersCount = stats.getClassMembers(className, ClassMemberType.FUNCTION).size();
        int inheritedMembersCount = stats.getClassInheritedMembers(className).size();
        int ownMembersCount = stats.getClassUniqueMembers(className).size();
        int overridingMembersCount = stats.getClassOverridingMembers(className).size();
        String baseClass = stats.getClassBase(className);

        if ( baseClass != null ) {
            classMap.put("baseClass", baseClass);
        }

        classMap.put("ownMembers", new Integer(ownMembersCount));
        classMap.put("overridingMembers", new Integer(overridingMembersCount));

        if ( areInheritedMembersReported() ) {
            nonfunctionMembersCount += stats.getClassInheritedMembers(className, ClassMemberType.NON_FUNCTION).size();
            functionMembersCount += stats.getClassInheritedMembers(className, ClassMemberType.FUNCTION).size();

            classMap.put("inheritedMembers", new Integer(inheritedMembersCount));
        }

        classMap.put("unknownMembers", new Integer(nonfunctionMembersCount));
        classMap.put("functionMembers", new Integer(functionMembersCount));

        if ( reportNamesOnly ) {
            classMap.put("members", membersMap);
            return classMap;
        }

        // non-function members first
        for ( String member : stats.getClassUniqueMembers(className, ClassMemberType.NON_FUNCTION) ) {
            membersMap.put(member, getClassMemberMap(className, member, stats));
        }

        for ( String member : stats.getClassOverridingMembers(className, ClassMemberType.NON_FUNCTION) ) {
            membersMap.put(member, getClassMemberMap(className, member, stats));
        }

        if ( areInheritedMembersReported() ) {
            for ( String member : stats.getClassInheritedMembers(className, ClassMemberType.NON_FUNCTION) ) {
                membersMap.put(member, getClassMemberMap(className, member, stats));
            }
        }

        // and then function members
        for ( String member : stats.getClassUniqueMembers(className, ClassMemberType.FUNCTION) ) {
            membersMap.put(member, getClassMemberMap(className, member, stats));
        }

        for ( String member : stats.getClassOverridingMembers(className, ClassMemberType.FUNCTION) ) {
            membersMap.put(member, getClassMemberMap(className, member, stats));
        }

        if ( areInheritedMembersReported() ) {
            for ( String member : stats.getClassInheritedMembers(className, ClassMemberType.FUNCTION) ) {
                membersMap.put(member, getClassMemberMap(className, member, stats));
            }
        }

        classMap.put("members", membersMap);

        return classMap;
    }


    @SuppressWarnings("unchecked")
    private Map getClassMemberMap(String className, String memberName, GlobalParsingStats stats) {
        Map memberMap = new LinkedHashMap();
        String upperClass = stats.findFirstUpperClassWithMember(className, memberName);
        boolean overriding = stats.isClassMemberOverriding(className, memberName);
        boolean inherited = stats.classHasInheritedMember(className, memberName);
        ClassMemberType type = stats.getClassMemberType(inherited ? upperClass : className, memberName);

        memberMap.put("type", type.getLabel());

        if ( inherited ) {
            memberMap.put("inheritedFrom", upperClass);
        }
        else if ( overriding ) {
            memberMap.put("overrides", upperClass + "." + memberName);
        }

        if ( areFileNamesReported() ) {
            Set<String> files = null;

            if ( inherited ) {
                files = stats.getFilesWhereClassMemberIsDefined(upperClass, memberName);
            }
            else {
                files = stats.getFilesWhereClassMemberIsDefined(className, memberName);
            }

            List<String> filesArray = new ArrayList<String>(files);
            memberMap.put("files", filesArray);
        }

        return memberMap;
    }


    @SuppressWarnings("unchecked")
    private void reportFunctions(GlobalParsingStats stats) {
        Set<String> functions = stats.getFunctions();
        Map functionsMap = new LinkedHashMap();

        for ( String function : functions ) {
            Map funMap = new LinkedHashMap();

            if ( areFileNamesReported() ) {
                String file = stats.getFunctionFile(function);
                funMap.put("file", file);
            }

            functionsMap.put(function, funMap);
        }

        printVariable(0, "functions", functionsMap);
    }


    @SuppressWarnings("unchecked")
    private void reportClosureObjects(GlobalParsingStats stats) {
        Set<String> objects = stats.getClosureObjects();
        Map objectsMap = new LinkedHashMap();

        for ( String object : objects ) {
            Map objMap = new LinkedHashMap();
            Set<String> unknownMembers = stats.getClosureObjectMembers(object, ClassMemberType.NON_FUNCTION);
            Set<String> functionMembers = stats.getClosureObjectMembers(object, ClassMemberType.FUNCTION);
            Map membersMap = new LinkedHashMap();

            objMap.put("unknownMembers", new Integer(unknownMembers.size()));
            objMap.put("functionMembers", new Integer(functionMembers.size()));

            if ( areFileNamesReported() ) {
                String file = stats.getClosureObjectFile(object);
                objMap.put("file", file);
            }

            if ( !reportNamesOnly ) {
                for ( String member : unknownMembers ) {
                    Map typeMap = new LinkedHashMap();
                    typeMap.put("type", "unknown");
                    membersMap.put(member, typeMap);
                }

                for ( String member : functionMembers ) {
                    Map typeMap = new LinkedHashMap();
                    typeMap.put("type", "function");
                    membersMap.put(member, typeMap);
                }
            }

            objMap.put("members", membersMap);

            objectsMap.put(object, objMap);
        }

        printVariable(0, "closureObjects", objectsMap);
    }


    private void printVariable(int indentLevel, String name, int value) {
        printVariable(indentLevel, name, new Integer(value));
    }


    private void printVariable(int indentLevel, String name, String[] value) {
        printVariable(indentLevel, name, Arrays.asList(value));
    }


    private void printVariable(int indentLevel, String name, Object value) {
        StringBuffer sb = new StringBuffer(indent(indentLevel));

        sb.append("var ");
        sb.append(name);
        sb.append(" = ");
        sb.append(printValue(indentLevel, value));
        sb.append(";");
        sb.append(NL(1));

        System.out.print(sb.toString());
    }


    @SuppressWarnings("unchecked")
    private String printValue(int indentLevel, Object value) {
        String result = null;

        if ( value == null ) {
            result = printNull();
        }
        else if (value instanceof String) {
            result = printValue((String) value);
        }
        else if (value instanceof Integer) {
            result = printValue((Integer) value);
        }
        else if (value instanceof List) {
            result = printValue(indentLevel, (List) value);
        }
        else if (value instanceof Map) {
            result = printValue(indentLevel + 1, (Map) value);
        }
        else {
            result = printUndef();
        }

        return result;
    }


    private String printValue(Integer value) {
        StringBuffer sb = new StringBuffer("");

        sb.append(value.toString());

        return sb.toString();
    }


    private String printValue(String value) {
        StringBuffer sb = new StringBuffer("");

        sb.append("'");
        sb.append(escapeString(value));
        sb.append("'");

        return sb.toString();
    }


    @SuppressWarnings("unchecked")
    private String printValue(int indentLevel, List value) {
        StringBuffer sb = new StringBuffer("");

        sb.append("[");

        for ( Iterator it = value.iterator(); it.hasNext(); ) {
            Object el = it.next();

            sb.append(printValue(indentLevel, el));

            if ( it.hasNext() ) {
                sb.append(", ");
            }
        }

        sb.append("]");


        return sb.toString();
    }


    @SuppressWarnings("unchecked")
    private String printValue(int indentLevel, Map value) {
        StringBuffer sb = new StringBuffer("");

        sb.append("{");

        if ( !value.isEmpty() ) {
            sb.append(NL(1));
        }

        for ( Iterator it = value.keySet().iterator(); it.hasNext(); ) {
            Object key = it.next();
            Object el = value.get(key);

            sb.append(indent(indentLevel));
            sb.append(printValue(key.toString()));
            sb.append(": ");
            sb.append(printValue(indentLevel, el));

            if ( it.hasNext() ) {
                sb.append(", ");
            }

            sb.append(NL(1));
        }

        if ( !value.isEmpty() ) {
            sb.append(indent(indentLevel - 1));
        }

        sb.append("}");

        return sb.toString();
    }


    private String printNull() {
        return "null";
    }


    private String printUndef() {
        return "undefined";
    }


    private String indent(int level) {
        StringBuffer sb = new StringBuffer("");

        for ( int i = 0; i < level; i++ ) {
            sb.append("    "); // 4 spaces
        }

        return sb.toString();
    }


    private String NL(int count) {
        StringBuffer sb = new StringBuffer("");

        for ( int i = 0; i < count; i++ ) {
            sb.append(NEWLINE);
        }

        return sb.toString();
    }


    private String escapeString(String str) {
        str = str.replaceAll("([^\\\\]|)\\\\", "$1\\\\\\\\");
        str = str.replaceAll("([^\\\\]|)'", "$1\\\\'");

        return str;
    }
}
